"use strict";

const _ = require("lodash");
const appRoot = require('app-root-path');
const PGMixin = require(appRoot + "/mixins/postgres.mixin");
const CacheCleanerMixin = require(appRoot + "/mixins/cache.cleaner.mixin");
const userModel = require(appRoot + "/pgmodels/userModel");
const {Op} = require('sequelize');

const { MoleculerClientError } = require("moleculer").Errors;
const utility = require("utility");   //密码加密
const jwt = require("jsonwebtoken");

module.exports = {
	name: "user",
	version:1,
	mixins:[
		PGMixin('user'),
		CacheCleanerMixin([
			"cache.clean.user",
		])
	],
	model: userModel,

	/**
	 * Settings
	 */
	settings: {
		auth: "required",
		JWT_SECRET: process.env.JWT_SECRET || "jwt-moleculer-example-secret",
		idField:'id',
		/** Public fields */
		fields: ["id","account","name","idCard","birthday","gender","email","mobile","address","postcode","superUser","isEnable"],
		//http://localhost:3000/api/v1/user?pageSize=20&page=1&search=1&searchFields=idCard,account&query={"account":"1","birthday":"2020-10-10"}
		excludeDBService:['remove','create','update'],
	},

	/**
	 * Dependencies
	 */
	dependencies: [],

	/**
	 * Actions
	 */
	actions: {


		/**
		 * Login with username & password
		 *
		 * @actions
		 * @param {Object} user - User credentials
		 *
		 * @returns {Object} Logged in user with token
		 */
		login:{
			auth:'ignore', //忽略验证，在 API service 中判断并验证
			rest:{
				method:'POST',
				path:'/login',
			},
			cache:false,
			params: {
				userName: { type:"string" },
				password: { type: "string" }
			},
			async handler(ctx) {
				const { userName,password } = ctx.params;
				let user = await this.adapter.model.findOne({
					attributes:["id","account","password","name","idCard","birthday","gender","email","mobile","address","postcode","superUser","isEnable"],
					where:{
						isDeleted:false,
						[Op.or]: [
							{ account: userName},
							{ email: userName }
						]
					},
				});
				if(!user){
					throw new MoleculerClientError("userName or password is invalid!", 422, "", [{ field: "message", message: "userName is not found" }]);
				}
				let passwordMd5 = utility.md5(password);
				if(passwordMd5 !== user.password){
					throw new MoleculerClientError("Wrong password!", 422, "", [{ field: "message", message: "Wrong password" }]);
				}
				const doc = await this.transformDocuments(ctx, {}, user);
				const { token } = this.transformEntity(doc, true, ctx.meta.token);
				return { token };
			}
		},


		/**
		 * Get user by JWT token (for API GW authentication)
		 *
		 * @actions
		 * @param {String} token - JWT token
		 *
		 * @returns {Object} Resolved user
		 */
		resolveToken: {
			cache: {
				keys: ["token"],
				ttl: 60 * 60 // 1 hour
			},
			params: {
				token: "string"
			},
			async handler(ctx) {
				const decoded = await new this.Promise((resolve, reject) => {
					jwt.verify(ctx.params.token, this.settings.JWT_SECRET, (err, decoded) => {
						if (err)
							return reject(err);
						resolve(decoded);
					});
				});
				if (decoded.id){
					let user = await this.getById(decoded.id);
					return await this.transformDocuments(ctx, {}, user);
				}
			}
		},


		/**
		 * Get current user entity.
		 * Auth is required!
		 *
		 * @actions
		 *
		 * @returns {Object} User entity
		 */
		me: {
			rest: "GET /me",
			cache: {
				keys: ["#account"]
			},
			async handler(ctx) {
				const user = await this.getById(ctx.meta.user["id"]);
				if (!user)
					throw new MoleculerClientError("User not found!", 400);
				const json = await this.transformEntity(user, true, ctx.meta.token);
				await this.entityChanged("updated", json, ctx);
				return await this.transformDocuments(ctx, {}, user);
			}
		},


		/**
		 * Update current user entity.
		 * Auth is required!
		 *
		 * @actions
		 *
		 * @param {Object} user - Modified fields
		 * @returns {Object} User entity
		 */
		updateMyself: {
			rest: "PUT /me",
			params:{
				user:{
					type:"object", props:{
						name:{ type:"string", min:2, max:50, optional:true},
						idCard:{ type:"string", min:2, max:20, optional:true, pattern:/^[0-9]+$/ },
						birthday:{ type:"string", min:2, max:20, optional:true },
						gender:{ type:"number", optional:true ,convert:true, integer:true, min:0, max:2 },
						email:{ type:"email", optional:true },
						mobile:{ type:"string", max:18, optional:true },
						address:{ type:"string", max:100, optional:true },
						postcode:{ type:"string", max:10, optional:true },
					}
				}
			},
			async handler(ctx) {
				const newData = ctx.params.user;
				if (newData.email) {
					const found = await this.adapter.findOne({ email: newData.email });
					if (found && found.id !== ctx.meta.user.id)
						throw new MoleculerClientError("Email is exist!", 422, "", [{ field: "email", message: "is exist" }]);
				}
				newData.updatedAt = new Date();
				const update = {
					"$set": newData
				};
				const doc = await this.adapter.updateById(Number(ctx.meta.user.id), update);
				const user = await this.transformDocuments(ctx, {}, doc);
				await this.entityChanged("updated", user, ctx);
				return user;
			}
		},

		/**
		 * Register a new user
		 *
		 * @actions
		 * @param {Object} user - User entity
		 *
		 * @returns {Object} Created entity
		 */
		createEntity: {
			rest: "POST /",
			params:{
				user:{
					type:"object", props:{
						account:{ type:"string", min:2, max:50, pattern:/^[a-zA-Z0-9]+$/ },
						password:{ type:"string", min:2, max:50, optional:true},
						name:{ type:"string", min:2, max:50, optional:true},
						idCard:{ type:"string", min:2, max:20, optional:true, pattern:/^[0-9]+$/ },
						birthday:{ type:"string", min:2, max:20, optional:true },
						gender:{ type:"number", optional:true ,convert:true, integer:true, min:0, max:2 },
						email:{ type:"email", optional:true },
						mobile:{ type:"string", max:18, optional:true },
						address:{ type:"string", max:100, optional:true },
						postcode:{ type:"string", max:10, optional:true },
					}
				}
			},
			async handler(ctx) {
				let entity = ctx.params.user;
				// await this.validateEntity(entity);
				if (entity.account) {
					const found = await this.adapter.findOne({
						where:{
							account:entity.account
						}
					});
					if (found)
						throw new MoleculerClientError("Username is exist!", 422, "", [{ field: "username", message: "is exist" }]);
				}
				if (entity.email) {
					const found = await this.adapter.findOne({
						where:{
							email: entity.email
						}
					});
					if (found)
						throw new MoleculerClientError("Email is exist!", 422, "", [{ field: "email", message: "is exist" }]);
				}
				entity.password = utility.md5(entity.password||'12345');
				entity.superUser = false;
				entity.isDeleted = false;
				entity.isEnable = true;
				entity.createdDate = new Date();
				const doc = await this.adapter.insert(entity);
				const user = await this.transformDocuments(ctx, {}, doc);
				await this.entityChanged("created", user, ctx);
				return user;
			}
		},

		/**
		 * update user
		 *
		 * @actions
		 * @param {Object} user - User entity
		 *
		 * @returns {Object} updated entity
		 */
		updateEntity: {
			rest: "PUT /",
			params:{
				user:{
					type:"object", props:{
						id:{ type:"number",convert:true, integer:true },
						account:{ type:"string", min:2, max:50, pattern:/^[a-zA-Z0-9]+$/ },
						password:{ type:"string", min:2, max:50, optional:true},
						name:{ type:"string", min:2, max:50, optional:true},
						idCard:{ type:"string", min:2, max:20, optional:true, pattern:/^[0-9]+$/ },
						birthday:{ type:"string", min:2, max:20, optional:true },
						gender:{ type:"number", optional:true ,convert:true, integer:true, min:0, max:2 },
						email:{ type:"email", optional:true },
						mobile:{ type:"string", max:18, optional:true },
						address:{ type:"string", max:100, optional:true },
						postcode:{ type:"string", max:10, optional:true },
						isEnable:{ type:"boolean", max:10, optional:true },
					}
				}
			},
			async handler(ctx) {
				let entity = ctx.params.user;
				// await this.validateEntity(entity);
				if (entity.account) {
					const found = await this.adapter.findOne({
						where:{
							account:entity.account
						}
					});
					if (found && found.id !== entity.id){
						throw new MoleculerClientError("Username is exist!", 422, "", [{ field: "username", message: "is exist" }]);
					}
				}

				if (entity.email) {
					const found = await this.adapter.findOne({
						where:{
							email: entity.email
						}
					});
					if (found && found.id !== entity.id){
						throw new MoleculerClientError("Email is exist!", 422, "", [{ field: "email", message: "is exist" }]);
					}
				}
				if(entity.password){
					entity.password = utility.md5(entity.password);
				}
				entity.updatedDate = new Date();
				const update = {
					"$set": entity
				};
				const doc = await this.adapter.updateById(Number(entity.id), update);
				const user = await this.transformDocuments(ctx, {}, doc);
				await this.entityChanged("created", user, ctx);
				return user;
			}
		},


		/**
		 * delete user
		 *
		 * @actions
		 * @param {Object} user - User entity
		 *
		 * @returns {Object} Created entity
		 */
		deleteEntity: {
			rest: "DELETE /:id",
			params:{
				id:{ type:"number",convert:true, integer:true },
			},
			async handler(ctx) {
				const user = await this.adapter.findById(Number(ctx.params.id));
				if(!user){
					throw new MoleculerClientError("User not found!", 400);
				}
				let result = await this.adapter.removeById(Number(ctx.params.id));
				await this.entityChanged("removed", user, ctx);
				return result;
			}
		},

	},

	/**
	 * Events
	 */
	events: {

	},

	/**
	 * Methods
	 */
	methods: {
		/**
		 * Generate a JWT token from user entity
		 *
		 * @param {Object} user
		 */
		generateJWT(user) {
			const today = new Date();
			const exp = new Date(today);
			exp.setDate(today.getDate() + 6000);
			return jwt.sign({
				id: user.id,
				username: user.account,
				exp: Math.floor(exp.getTime() / 1000)
			}, this.settings.JWT_SECRET);
		},

		/**
		 * Transform returned user entity. Generate JWT token if necessary.
		 *
		 * @param {Object} user
		 * @param {Boolean} withToken
		 */
		transformEntity(user, withToken, token) {
			if (user) {
				if (withToken)
					user.token = token || this.generateJWT(user);
			}
			return user ;
		},

		/**
		 * Transform returned user entity as profile.
		 *
		 * @param {Context} ctx
		 * @param {Object} user
		 * @param {Object?} loggedInUser
		 */
		async transformProfile(ctx, user, loggedInUser) {
			//user.image = user.image || "https://www.gravatar.com/avatar/" + crypto.createHash("md5").update(user.email).digest("hex") + "?d=robohash";
			user.image = user.image || "https://static.productionready.io/images/smiley-cyrus.jpg";

			if (loggedInUser) {
				const res = await ctx.call("follows.has", { user: loggedInUser._id.toString(), follow: user._id.toString() });
				user.following = res;
			} else {
				user.following = false;
			}

			return { profile: user };
		}
	},

	/**
	 * Service created lifecycle event handler
	 */
	created() {

	},

	/**
	 * Service started lifecycle event handler
	 */
	async started() {

	},

	/**
	 * Service stopped lifecycle event handler
	 */
	async stopped() {

	}
};
